iT邦幫忙

0

【You Don't Know JS: Scope & Closures】Chapter 3 筆記

WM 2019-03-02 23:10:381603 瀏覽
  • 分享至 

  • xImage
  •  

Chapter 2談過,每個範疇(scope)就像容器一樣,裡面包含各種的識別子(變數、函式)。

巢狀範疇(nested scopes)在撰寫程式碼時期就已經定義了。

Scope From Functions

一般認為,在JavaScript中,範疇(scope)是以函式為基礎。

宣告函式的同時,也建立範疇(scope),但並不是只有函式能建立範疇(scope)。

Example

function foo(a) {
    var b = 2;   
    function bar() {       
    }    
    var c = 3;
}

foo( )中,包含了識別字a、b、c、bar,這些都在foo( )的範疇之中。

全域範疇中只有一個識別字:foo

因為a、b、c、bar是屬於foo( )的範疇,所以在foo( )的外面(全域範疇)是無法存取到這些識別字的:

bar(); //失敗

console.log( a, b, c ); //失敗

以上所有的識別字都能在foo( )內部存取,如此一來,也能運用到JS變數的「動態」本質,接收不同型別的值。

隱藏在普通範疇中

利用函式範疇的特性,將我們的程式碼包在函式裡面,建立起如同「隱藏」的效果,讓外面無法窺探內部的運作。

這種隱藏的手段,主要是來自於軟體設計原理「最小權限原則(Principle of Least Privilege)」。

這個原則指出,我們設計的模組或是API,應該只能透露出最少的細節,並把其他的變數或函式給隱藏起來。

如果我們將所有的變數或函式都宣告於全域範疇之中,那他們都能被其他的內嵌的範疇存取,這會違反最小權限原則,如此一來,我們很有可能會透露應該保有私密性的變數或函式。

function doSomething(a) {
	b = a + doSomethingElse( a * 2 );
	console.log( b * 3 );
}
function doSomethingElse(a) {
	return a - 1;
}
var b;
doSomething( 2 ); // 15

bdoSomethingElse( )本應是doSomething( )內部的工作細節,但它們宣告於全域範疇中,意味著,在doSomething( )之外也能存取它們,這容易造成非預期的情況發生。正確的作法是應該將那些細節隱藏在doSomething( )之中。

function doSomething(a) {
	function doSomethingElse(a) {
		return a - 1;
	}
	var b;
	b = a + doSomethingElse( a * 2 );
	console.log( b * 3 );
}
doSomething( 2 ); // 15

如此一來,bdoSomethingElse( )完全存在於doSomething( )的範疇之中,外界無法控制它們,功能也沒有改變,這種設計方式通常被認為是較佳的。

避免衝突

將特定的識別字隱藏在一個範疇之中的另一個好處是,避免同名但不同用途的識別字發生衝突。

function foo() {
	function bar(a) {
		i = 3; //會變更for迴圈的i
		console.log( a + i );
	}
	for (var i=0; i<10; i++) {
		bar( i * 2 ); //無窮迴圈
	}
}
foo();

bar( )內部的i會意外地影響迴圈的i,會讓i永遠為3,導致無窮迴圈。解決方式是在bar( )內部改成var i = 3;,讓i成為bar( )內的區域變數,而不會影響到for迴圈的另一個i

全域命名空間

另一個可能發生衝突的情況是,當你的程式載入其他的程式庫,卻沒有正確地隱藏程式庫的私有識別字。

這些程式庫通常會在全域範疇宣告變數或函式,有可能是一個物件。這個物件會被當作命成空間使用,所有要對外的功能都會是物件的屬性,而非宣告在語意範疇中頂層的識別字。

Example

var MyReallyCoolLibrary = {
	awesome: "stuff",
	doSomething: function() {
		// ...
	},
	doAnotherThing: function() {
		// ...
	}
};

函式作為範疇

剛剛已說明我們可以將識別字宣告於函式中,建立其範疇,不讓外面範疇存取。

Example

var a = 2;
function foo() {
	var a = 3;
	console.log( a ); // 3
}
foo();
console.log( a ); // 2

以上的方式是合理的,但有些問題。首先,我們必須要宣告函式名稱foo,這本身會汙染到全域範疇。再者,我們還得呼叫foo函式,裡面的程式碼才會執行。

有個方式可以讓該函式不需要名稱(或許就不會汙染到包含它的範疇),並且能夠自動執行:

var a = 2;
(function foo(){
	var a = 3;
	console.log( a ); // 3
})();
console.log( a ); // 2

首先我們看到整個函式被包在( )內部,這表示該函式不會被當成函式宣告,而是一個函式運算式(function-expression)。

要分辨宣告與運算式的方法,是看function的位置。若function在敘述句的最開頭,那它就是函式宣告;若不是,就是函式運算式。

匿名 VS. 具名

setTimeout( function(){
	console.log("I waited 1 second!");
}, 1000 );

上面的方式被稱為匿名函式運算式(anonymous function expression)。函式運算式可以匿名,但函式宣告不行。

但函式運算式有幾個缺點:
1.函式運算式在堆疊軌跡沒有名字可以顯示,會造成除錯的困難。
2.若函式進行遞迴的話,會得到已被棄用的arguments.callee參考。
3.函式名稱可以增加我們的程式可讀性。

行內涵式運算式(Inline function expressions),可以解決上述問題,是最佳的實務做法:

setTimeout( function timeoutHandler(){ // <--增加函式名稱
	console.log( "I waited 1 second!" );
}, 1000 );

立即調用函式運算式

var a = 2;

(function foo(){
	var a = 3;
	console.log( a ); // 3
})();

將函式包在( )之中,並且在尾部加再上()來執行這個運算式。

第1個()將函式變成運算式,第2個()直接執行該函式。

這種方式很普遍,通稱為立即調用函式運算式(IIFE,Immediately Invoked Function Expression)。

IIFE可以使用匿名函式,但若為函式命名,可以減去一些麻煩。

IIFE有另一種表示方式,(function(){ .. }()),直接把執行的第2個()包在第1個()裡面,以上的2種方式功能都一樣。

IIFE也可以傳入引數:

var a = 2;
(function IIFE( global ){
	var a = 3;
	console.log( a ); // 3,區域變數a
	console.log( global.a ); // 2,全域變數a
})( window );
console.log( a ); // 2

IIFE( )內外各宣告變數a,並傳入一個window物件,如此一來,我們可以區分全域參考與區域參考的差異。

let

let與varㄧ樣都是宣告變數的關鍵字,不同的是,以let宣告的變數,其範疇是以區塊{}作為基準。

var foo = true;
if (foo) {
	let bar = foo * 2;
	bar = something( bar );
	console.log( bar );
}
console.log( bar ); // 擲出ReferenceError

因為以let宣告的變數bar,它的範疇限制在if敘述句的大括號{ }中,意味著在此範疇外的環境無法存取bar

let 迴圈

for (let i=0; i<10; i++) {
	console.log( i );
}
console.log( i ); // ReferenceError

在for迴圈使用let宣告,let不只會將i繫結到for主體,還會重新繫結迴圈每跑一次的值,會將前一個i值再重新指定給i。

const

const也會建立以區塊{}為範疇的變數,比較特別的是,宣告時就必須要一併給值,其值是固定不變的,之後任何改變值的行為都會擲出錯誤。

參考來源:
https://ithelp.ithome.com.tw/upload/images/20181117/20112573OWtzPwjWh4.jpg

此為You Don't Know JS系列的筆記。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言